-
Notifications
You must be signed in to change notification settings - Fork 36
fixed crash in the thread pool occurs because of enqueueing tasks before the thread pool starts #4583
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
fixed crash in the thread pool occurs because of enqueueing tasks before the thread pool starts #4583
Conversation
|
В каких условиях сработала верифайка - в каких-то UT? |
| future.GetValueSync(); | ||
|
|
||
| // Sleep to be sure that task will be enqueued before start. | ||
| Sleep(TDuration::Seconds(1)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
А если вызвать promise.SetValue(); после threadPool->Execute ?
Можно взять latch для синхронизации:
auto threadPool = CreateThreadPool("thread", 1);
std::latch enqueued{1};
std::thread thread([&] {
auto future = threadPool->Execute([] { return 42; });
enqueued.count_down();
UNIT_ASSERT_EQUAL(42, future.GetValue(WaitTimeout));
});
enqueued.wait();
threadPool->Start();
threadPool->Stop();
thread.join();There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
я хотел написать тест который будет крашиться в отсутствии фикса.
Если фикса нет, то threadPool->Execute зависнет пока не вызовется threadPool->Start() т.е. будет дедлок.
Как бы да зафейлится тест но хотелось бы краш
по хорошему надо было бы вставить count_down после AllocateWorker
nbs/cloud/storage/core/libs/common/thread_pool.cpp
Lines 120 to 127 in 155b318
| void Enqueue(ITaskPtr task) override | |
| { | |
| Queue.Enqueue(std::move(task)); | |
| if (AllocateWorker()) { | |
| WakeUpWorker(); | |
| } | |
| } |
но так сделать понятно не получится. Так что не понятно как воспроизвести стабильно краш без слипов и без того чтобы лезть в кишки тред пула
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
enqueued.wait();
// Sleep to make it more likely that ReleaseWorker is called before starting the thread pool
Sleep(1s);
threadPool->Start();
Написал интеграционные тесты для фичи с открытием закрытием девайсов в диск агенте ( код тот же что и в пре #4299) |
| , SpinCycles(DurationToCyclesSafe(SPIN_TIMEOUT)) | ||
| , MemoryTagScope(std::move(memoryTagScope)) | ||
| , Workers(numWorkers) | ||
| , RunningWorkers(NumWorkers) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Кажется теперь название не соответствует действительности. В конструкторе воркеры не запускаются. Ну и да, очень подозрительно выглядит сценарий когда мы начинаем юзать пул без вызова Start (у нас тогда любой IStartable по идее может страдать от такого же). Если кто-то так делает, то лучше наверное эту проблему решать в Execute (бросать исключение, ждать на фьюче до вызова старт), но в общем попытка использования threadpool без вызова Start выглядит как нарушение "контракта"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Не понимаю, почему это нарушение контракта. Тот же шедулер (https://github.com/ydb-platform/nbs/blob/6cfc50c76085138d8908ffa4bdef8bb3ecb22f4a/cloud/blockstore/libs/daemon/common/bootstrap.cpp#L966C28-L966C37) стартует самым последним, и любая компонента может зашедулить таски в него до его старта, тред пул — очень похожая по смыслу штука. Тем более у нас в компонентах циклы по зависимостям, и как бэ наоборот надо стремиться к тому, чтобы все компоненты корректно себя вели, если их ручки дергают до старта, а не крашить процесс исключением, если такое происходит. Да и в целом есть 2 однострочных фикса (одинаковых по сложности): один из них расширяет допустимые сценарии использования объекта, убирает возможность случайно совершить ошибку, а второй опирается на какие-то совершенно неочевидные контракты(которые к одним компонентам приложимы, к другим нет) и фиксит только поведение внутри моего пра, при этом кто-то через N времени может заиспользовать случайно тред пул до его старта и получить исключение и, соответственно, краш. И ладно, если это произойдет при тестировании, такое может и до прода успеть доехать и крашнуть процесс там, я уже фиксил баги, которые стреляют только в проде, если какую-то из ручек дернули слишком рано на старте процесса, так что такой сценарий мне кажется довольно вероятным
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ну а насчет названия, оно и до этого не сильно соответствовало реальности. Тип этот атомик означает кол-во воркеров, которые не ждут задачки в функции wait, по сути. Можно, конечно, переименовать, но тогда уж лучше отдельным пром наверное, или изменить RunningWorkers на WaitingWorkers
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Откуда берётся сценарий использования трэд пула до его старта?
Такого не должно происходит, если происходит - нужно чинить вызывающий код
Плюс в Enqueue должна быть верифайка что thread pool в состоянии started
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Откуда берётся сценарий использования трэд пула до его старта?
Такого не должно происходит, если происходит - нужно чинить вызывающий код
Почему? У нас многие компоненты используются до старта. Тот же шедулер, который я уже упомянул.
Там прям целый отдельный комент что так задумано и что у нас есть циклы в зависимостях, а если есть циклы то есть и теоритическая вероятность, что кто-то что-то заиспользует до старта.
nbs/cloud/blockstore/libs/daemon/common/bootstrap.cpp
Lines 961 to 966 in e4faf1a
| // we need to start scheduler after all other components for 2 reasons: | |
| // 1) any component can schedule a task that uses a dependency that hasn't | |
| // started yet | |
| // 2) we have loops in our dependencies, so there is no 'correct' starting | |
| // order | |
| START_COMMON_COMPONENT(Scheduler); |
помимо шедулера в акторную систему передаются RdmaClient, EndpointManager(через EndpointEventHandler)
nbs/cloud/blockstore/libs/daemon/common/bootstrap.cpp
Lines 945 to 959 in e4faf1a
| START_KIKIMR_COMPONENT(ActorSystem); | |
| START_COMMON_COMPONENT(EndpointManager); | |
| START_COMMON_COMPONENT(Service); | |
| START_COMMON_COMPONENT(VhostServer); | |
| START_COMMON_COMPONENT(NbdServer); | |
| START_COMMON_COMPONENT(GrpcEndpointListener); | |
| START_COMMON_COMPONENT(Executor); | |
| START_COMMON_COMPONENT(Server); | |
| START_COMMON_COMPONENT(ServerStatsUpdater); | |
| START_COMMON_COMPONENT(BackgroundThreadPool); | |
| START_COMMON_COMPONENT(RdmaClient); | |
| START_COMMON_COMPONENT(GetTraceServiceClient()); | |
| START_COMMON_COMPONENT(RdmaRequestServer); | |
| START_COMMON_COMPONENT(RdmaTarget); | |
| START_COMMON_COMPONENT(CellManager); |
которые стартуют позже акторной системы
CellManager стартует самым последним почти хоть и передается в другие компоненты которые стартуют раньше.
Особенно вот вообще неочевидно почему для шедулера это допустимо и нормально, а для тред пула это недопустимое поведение.
Тем более сделать так чтобы можно было использовать тред пул до старта легко, буквально перетащить одну строчку, так зачем добавлять какую-то верифайку, на которую можно наткнуться максимально неожиданным способом причем в проде.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Тем более сделать так чтобы можно было использовать тред пул до старта легко, буквально перетащить одну строчку, так зачем добавлять какую-то верифайку, на которую можно наткнуться максимально неожиданным способом причем в проде.
А что с остановкой? Если тредпул используется после остановки - это явная ошибка.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
А что с остановкой? Если тредпул используется после остановки - это явная ошибка.
Я тут в целом согласен, что это может привести к неожиданным эффектам, из-за того что задача не исполнится. То есть код, который постит что-то в остановленный тред пул, лучше не множить и подсветить разботчику что он делает что-то не то.
Поэтому кажется достаточно Y_DEBUG_ABORT_UNLESS в этом месте. В проде взрываться смысла не вижу, т.к. критичность у таких багов низкая
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Поговорили голосом. Перемещение RunningWorkers в конструктор, формально, не вносит неконсистентность в код, а даже исправляет её, потому что в конструкторе инициализируются Workers в стейте RUNNING, поэтому логично инициализировать RunningWorkers == Workers.size() == NumWorkers.
При этом остаётся формальная неконсистентность потому что после запуска конструктора воркеры не совсем running, ибо требуется вызов Start чтобы функция воркера начала выполнение. Но это минор
|
Note This is an automated comment that will be appended during run. 🔴 linux-x86_64-relwithdebinfo: some tests FAILED for commit dac2122.
🟢 linux-x86_64-relwithdebinfo: all tests PASSED for commit dac2122.
|
|
Note This is an automated comment that will be appended during run. 🔴 linux-x86_64-relwithdebinfo: some tests FAILED for commit e4faf1a.
🟢 linux-x86_64-relwithdebinfo: all tests PASSED for commit e4faf1a.
|
|
Note This is an automated comment that will be appended during run. 🔴 linux-x86_64-relwithdebinfo: some tests FAILED for commit e4faf1a.
🟢 linux-x86_64-relwithdebinfo: all tests PASSED for commit e4faf1a.
|
|
Note This is an automated comment that will be appended during run. 🟢 linux-x86_64-relwithdebinfo: all tests PASSED for commit 1168b9a.
🟢 linux-x86_64-relwithdebinfo: all tests PASSED for commit 1168b9a.
|
|
|
||
| void Enqueue(ITaskPtr task) override | ||
| { | ||
| Y_ABORT_UNLESS(AtomicGet(ShouldStop) == 0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Почему не DEBUG? Зачем взрываться в проде на этом?
Сейчас возможен следующий сценарий:
Пусть есть два треда T1, T2 и Thread Pool с одним воркером
изначально до запуска пула RunningWorkers равен 0
nbs/cloud/storage/core/libs/common/thread_pool.cpp
Line 72 in 155b318
nbs/cloud/storage/core/libs/common/thread_pool.cpp
Lines 120 to 127 in 155b318
nbs/cloud/storage/core/libs/common/thread_pool.cpp
Lines 269 to 275 in 155b318
nbs/cloud/storage/core/libs/common/thread_pool.cpp
Lines 98 to 105 in 155b318
nbs/cloud/storage/core/libs/common/thread_pool.cpp
Lines 130 to 145 in 155b318
nbs/cloud/storage/core/libs/common/thread_pool.cpp
Line 280 in 155b318
nbs/cloud/storage/core/libs/common/thread_pool.cpp
Lines 130 to 145 in 155b318